﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using MvcSiteMap.Core.Extensions;

namespace MvcSiteMap.Core.Helpers
{
    /// <summary>
    /// HtmlHelper extension methods
    /// </summary>
    public static class MenuHelper
    {
        /// <summary>
        /// Build a menu, based on the MvcSiteMap
        /// </summary>
        /// <param name="helper">HtmlHelper instance</param>
        /// <param name="selectedMenuItemCssClass">Selected menu item CSS class</param>
        /// <returns>Html markup</returns>
        public static string Menu(this HtmlHelper helper, string selectedMenuItemCssClass)
        {
            return Menu(helper, true, false, selectedMenuItemCssClass, null);
        }

        /// <summary>
        /// Build a menu, based on the MvcSiteMap
        /// </summary>
        /// <param name="helper">HtmlHelper instance</param>
        /// <param name="selectedMenuItemCssClass">Selected menu item CSS class</param>
        /// <param name="menuHtmlAttributes">Html attributes for the outer unordered list</param>
        /// <returns>Html markup</returns>
        public static string Menu(this HtmlHelper helper, string selectedMenuItemCssClass, object menuHtmlAttributes)
        {
            return Menu(helper, true, false, selectedMenuItemCssClass, menuHtmlAttributes);
        }

        /// <summary>
        /// Build a menu, based on the MvcSiteMap
        /// </summary>
        /// <param name="helper">HtmlHelper instance</param>
        /// <param name="showStartingNode">Show starting node?</param>
        /// <param name="startFromCurrentNode">Start from current node?</param>
        /// <param name="selectedMenuItemCssClass">Selected menu item CSS class</param>
        /// <param name="menuHtmlAttributes">Html attributes for the outer unordered list</param>
        /// <returns>Html markup</returns>
        public static string Menu(this HtmlHelper helper, bool showStartingNode, bool startFromCurrentNode, string selectedMenuItemCssClass, object menuHtmlAttributes)
        {
            return Menu(helper, SiteMap.Provider.Name, showStartingNode, startFromCurrentNode, selectedMenuItemCssClass, 1, menuHtmlAttributes);
        }

        /// <summary>
        /// Build a menu, based on the MvcSiteMap
        /// </summary>
        /// <param name="helper">HtmlHelper instance</param>
        /// <param name="providerName">Provider name (can be used to specify SiteMap datasource)</param>
        /// <param name="selectedMenuItemCssClass">Selected menu item CSS class</param>
        /// <returns>Html markup</returns>
        public static string Menu(this HtmlHelper helper, string providerName, string selectedMenuItemCssClass)
        {
            return Menu(helper, providerName, true, false, selectedMenuItemCssClass, 1, null);
        }

        /// <summary>
        /// Build a menu, based on the MvcSiteMap
        /// </summary>
        /// <param name="helper">HtmlHelper instance</param>
        /// <param name="providerName">Provider name (can be used to specify SiteMap datasource)</param>
        /// <param name="selectedMenuItemCssClass">Selected menu item CSS class</param>
        /// <param name="menuHtmlAttributes">Html attributes for the outer unordered list</param>
        /// <returns>Html markup</returns>
        public static string Menu(this HtmlHelper helper, string providerName, string selectedMenuItemCssClass, int maxLevels, object menuHtmlAttributes)
        {
            return Menu(helper, providerName, true, false, selectedMenuItemCssClass, maxLevels, menuHtmlAttributes);
        }



        /// <summary>
        /// Build a menu, based on the MvcSiteMap
        /// </summary>
        /// <param name="helper">HtmlHelper instance</param>
        /// <param name="providerName">Provider name (can be used to specify SiteMap datasource)</param>
        /// <param name="showStartingNode">Show starting node?</param>
        /// <param name="startFromCurrentNode">Start from current node?</param>
        /// <param name="selectedMenuItemCssClass">Selected menu item CSS class</param>
        /// <param name="menuHtmlAttributes">Html attributes for the outer unordered list</param>
        /// <returns>Html markup</returns>
        public static string Menu(this HtmlHelper helper, string providerName, bool showStartingNode, bool startFromCurrentNode, string selectedMenuItemCssClass, int maxLevels, object menuHtmlAttributes)
        {
            return Menu(helper, providerName, showStartingNode, 0, startFromCurrentNode, selectedMenuItemCssClass, maxLevels, menuHtmlAttributes, -1);
        }

        /// <summary>
        /// Build a menu, based on the MvcSiteMap
        /// </summary>
        /// <param name="helper">HtmlHelper instance</param>
        /// <param name="startingNodeLevel">Starting node level</param>
        /// <param name="selectedMenuItemCssClass">Selected menu item CSS class</param>
        /// <returns>Html markup</returns>
        public static string Menu(this HtmlHelper helper, int startingNodeLevel, string selectedMenuItemCssClass)
        {
            return Menu(helper, SiteMap.Provider.Name, false, startingNodeLevel, true, selectedMenuItemCssClass, 1, null, -1);
        }

        /// <summary>
        /// Build a menu, based on the MvcSiteMap
        /// </summary>
        /// <param name="helper">HtmlHelper instance</param>
        /// <param name="startingNodeLevel">Starting node level</param>
        /// <param name="selectedMenuItemCssClass">Selected menu item CSS class</param>
        /// <param name="menuHtmlAttributes">Html attributes for the outer unordered list</param>
        /// <returns>Html markup</returns>
        public static string Menu(this HtmlHelper helper, int startingNodeLevel, string selectedMenuItemCssClass, object menuHtmlAttributes)
        {
            return Menu(helper, SiteMap.Provider.Name, false, startingNodeLevel, true, selectedMenuItemCssClass, 1, menuHtmlAttributes, -1);
        }

        /// <summary>
        /// Build a menu, based on the MvcSiteMap
        /// </summary>
        /// <param name="helper">HtmlHelper instance</param>
        /// <param name="providerName">Provider name (can be used to specify SiteMap datasource)</param>
        /// <param name="startingNodeLevel">Starting node level</param>
        /// <param name="selectedMenuItemCssClass">Selected menu item CSS class</param>
        /// <returns>Html markup</returns>
        public static string Menu(this HtmlHelper helper, string providerName, int startingNodeLevel, string selectedMenuItemCssClass)
        {
            return Menu(helper, providerName, false, startingNodeLevel, true, selectedMenuItemCssClass, 1, null, -1);
        }

        /// <summary>
        /// Build a menu, based on the MvcSiteMap
        /// </summary>
        /// <param name="helper">HtmlHelper instance</param>
        /// <param name="providerName">Provider name (can be used to specify SiteMap datasource)</param>
        /// <param name="startingNodeLevel">Starting node level</param>
        /// <param name="selectedMenuItemCssClass">Selected menu item CSS class</param>
        /// <param name="menuHtmlAttributes">Html attributes for the outer unordered list</param>
        /// <returns>Html markup</returns>
        public static string Menu(this HtmlHelper helper, string providerName, int startingNodeLevel, string selectedMenuItemCssClass, object menuHtmlAttributes)
        {
            return Menu(helper, providerName, false, startingNodeLevel, true, selectedMenuItemCssClass, 1, menuHtmlAttributes, -1);
        }

        /// <summary>
        /// Build a pinned menu, based on the MvcSiteMap
        /// </summary>
        /// <param name="helper">HtmlHelper instance</param>
        /// <param name="providerName">Provider name (can be used to specify SiteMap datasource)</param>
        /// <param name="pinnedLevel">Pinned level</param>
        /// <param name="menuHtmlAttributes">Html attributes for the outer unordered list</param>
        /// <returns>Html markup</returns>
        public static string MenuPinned(this HtmlHelper helper, string providerName, int pinnedLevel, object menuHtmlAttributes)
        {
            return Menu(helper, providerName, false, 0, true, string.Empty, -1, menuHtmlAttributes, pinnedLevel);
        }

        /// <summary>
        /// Build a menu, based on the MvcSiteMap
        /// </summary>
        /// <param name="helper">HtmlHelper instance</param>
        /// <param name="providerName">Provider name (can be used to specify SiteMap datasource)</param>
        /// <param name="showStartingNode">Show starting node?</param>
        /// <param name="startFromCurrentNode">Start from current node?</param>
        /// <param name="startingNodeLevel">Starting node level</param>
        /// <param name="selectedMenuItemCssClass">Selected menu item CSS class</param>
        /// <param name="menuHtmlAttributes">Html attributes for the outer unordered list</param>
        /// <returns>Html markup</returns>  
        public static string Menu(this HtmlHelper helper, string providerName, bool showStartingNode, int startingNodeLevel, bool startFromCurrentNode, string selectedMenuItemCssClass, int maxLevels, object menuHtmlAttributes, int pinnedNodeLevel)
        {
            SiteMapProvider selectedSiteMapProvider = SiteMap.Providers[providerName];
            // Check for provider
            if (selectedSiteMapProvider == null)
            {
                throw new UnknownSiteMapProviderException(string.Format("Unknown SiteMap provider: {0}", providerName));
            }

            // Helpers
            UrlHelper urlHelper = new UrlHelper(helper.ViewContext.RequestContext);

            // Html attributes
            IDictionary<string, object> attributes = new RouteValueDictionary(menuHtmlAttributes);

            // Determine root node
            SiteMapNode relativeRootNode = null;
            if (startFromCurrentNode)
            {
                relativeRootNode = selectedSiteMapProvider.CurrentNode;
                if (relativeRootNode == null)
                    relativeRootNode = selectedSiteMapProvider.RootNode;
                else if (pinnedNodeLevel>0)
                {
                    while (GetNodeLevel(relativeRootNode)>pinnedNodeLevel)
                    {
                        relativeRootNode = relativeRootNode.ParentNode;
                    }
                }
            }
            else
            {
                relativeRootNode = selectedSiteMapProvider.RootNode;
            }


            // Node not defined?
            if (relativeRootNode == null)
            {
                // String builder
                StringBuilder sb1 = new StringBuilder();
                CreateUnorderedListTag(helper, selectedMenuItemCssClass, selectedSiteMapProvider, urlHelper, attributes, new SiteMapNodeCollection(), sb1, maxLevels);
                return sb1.ToString();
            }


            // Fetch nodes to render
            SiteMapNodeCollection nodesToRender = new SiteMapNodeCollection();
            if (showStartingNode)
            {
                nodesToRender.Add(relativeRootNode);
            }
            if (relativeRootNode.HasChildNodes)
            {
                // Use startingNodeLevel?
                if (startingNodeLevel == 0)
                {
                    // no...
                    nodesToRender.AddRange(relativeRootNode.ChildNodes);
                }
                else
                {
                    // yes: determine absolute level of relativeRootNode
                    int startingNodeAbsoluteLevel = GetNodeLevel(relativeRootNode);
                    if (startingNodeAbsoluteLevel + 1 == startingNodeLevel)
                    {
                        nodesToRender.AddRange(relativeRootNode.ChildNodes);
                    }
                }
            }


            // String builder
            StringBuilder sb = new StringBuilder();
            CreateUnorderedListTag(helper, selectedMenuItemCssClass, selectedSiteMapProvider, urlHelper, attributes, nodesToRender, sb, maxLevels);
            return sb.ToString();
        }

        /// <summary>
        /// Creates a UL tag
        /// </summary>
        /// <param name="helper">HtmlHelper instance</param>
        /// <param name="selectedMenuItemCssClass">Selected MenuItem Css Class</param>
        /// <param name="selectedSiteMapProvider">Selected SiteMapProvider</param>
        /// <param name="urlHelper">UrlHelper instance</param>
        /// <param name="attributes">Attributes</param>
        /// <param name="nodesToRender">List of nodes to render</param>
        /// <param name="sb">StringBuilder to work with</param>
        /// <param name="maxLevels">Maximal number of levels</param>
        private static void CreateUnorderedListTag(HtmlHelper helper, string selectedMenuItemCssClass, SiteMapProvider selectedSiteMapProvider, UrlHelper urlHelper, IDictionary<string, object> attributes, SiteMapNodeCollection nodesToRender, StringBuilder sb, int maxLevels) 
        {
            // Create unordered list tag
            sb.Append("<ul");
            if (attributes != null)
            {
                foreach (var attribute in attributes)
                {
                    sb.Append(string.Format(" {0}=\"{1}\"", attribute.Key, attribute.Value));
                }
            }
            sb.AppendLine(">");

            // Render each node
            foreach (SiteMapNode node in nodesToRender)
            {
                MvcSiteMapNode mvcNode = node as MvcSiteMapNode;
                bool nodeVisible = true;

                // Node authorized?
                if (!node.IsAccessibleToUser(HttpContext.Current))
                    nodeVisible = false;

                // Node visible?
                if (mvcNode != null && mvcNode.Visibility != MvcSiteMapNodeVisibility.Full)
                    nodeVisible = false;

                // Render node
                if (nodeVisible)
                {
                    if (selectedSiteMapProvider.CurrentNode != null &&
                            (selectedSiteMapProvider.CurrentNode.Equals(node)
                                || (selectedSiteMapProvider.CurrentNode.IsDescendantOf(node) && !node.Equals(selectedSiteMapProvider.RootNode))
                            )
                       )
                    {
                        sb.Append("<li");
                        if (!string.IsNullOrEmpty(selectedMenuItemCssClass))
                        {
                            sb.Append(string.Format(" class=\"{0}\"", selectedMenuItemCssClass));
                        }
                        sb.Append(">");
                    }
                    else
                    {
                        sb.Append("<li>");
                    }

                    string url = string.Empty;
                    if (!string.IsNullOrEmpty(node.Url))
                        url = urlHelper.Content(node.Url);

                    string imageMarkup = string.Empty;
                    if (mvcNode != null && !string.IsNullOrEmpty(mvcNode.ImageUrl))
                    {
                        string imageUrl = urlHelper.Content(mvcNode.ImageUrl);
                        imageMarkup = string.Format("<img src=\"{0}\" class=\"icon\" alt=\"{1}\" />", imageUrl, helper.Encode(node.Title));
                    }


                    // Determine extra attributes
                    StringBuilder extraAttributes = new StringBuilder();
                    if (mvcNode != null)
                    {
                        if (!string.IsNullOrEmpty(mvcNode.Target))
                        {
                            extraAttributes.Append(" target=\"" + mvcNode.Target + "\"");
                        }
                        if (!string.IsNullOrEmpty(mvcNode.Title))
                        {
                            extraAttributes.Append(" title=\"" + mvcNode.Title + "\"");
                        }
                    }

                    sb.AppendFormat("<a href=\"{0}\"{1}>{2}{3}</a>", url, extraAttributes, imageMarkup, helper.Encode(node.Title));

                    if (maxLevels != 1 && node.HasChildNodes)
                        CreateUnorderedListTag(helper, selectedMenuItemCssClass, selectedSiteMapProvider, urlHelper, null, node.ChildNodes, sb, maxLevels--);

                    sb.AppendLine("</li>");
                }
            }

            // Close unordered list tag
            sb.AppendLine("</ul>");
        }

        /// <summary>
        /// Get SiteMapNode level
        /// </summary>
        /// <param name="node">SiteMapNode to determine level for</param>
        /// <returns>SiteMapNode level</returns>
        public static int GetNodeLevel(SiteMapNode node)
        {
            int level = 0;
            while (node.ParentNode != null)
            {
                level++;
                node = node.ParentNode;
            }
            return level;
        }
    }
}